package curvilinear;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;

import model.CurvilinearSelectionModel;

import controller.CurvilinearSelectionController;

import view.IView;

/**
 * This class is a panel that holds the curvilinear image. It implements
 * MouseListener so it can detect where the user clicks when selecting certain
 * ROI parameters. This class contains the logic for when the user is selecting
 * ROI parameters and sets the parameters on the selection model
 * 
 */
public class CurvilinearImagePanel extends JLayeredPane implements MouseListener, IView {

    private static final long serialVersionUID = 2642729249633052940L;

    CurvilinearSelectionController controller;
    CurvilinearSelectionModel model;
    BufferedImage img;
    JPanel selectionPanel;
    JPanel selectionPointsPanel;

    JLabel imageLabel;

    private boolean selectingCircle;
    private boolean selectingWidth; 
    private boolean selectingRadii;

    List<Point> curPoints;

    public CurvilinearImagePanel(BufferedImage img, CurvilinearSelectionController controller, CurvilinearSelectionModel model) {
        this.controller = controller;
        this.model = model;
        this.img = img;
        curPoints = new ArrayList<Point>();
        selectionPanel = new SelectionPanel();
        selectionPointsPanel = new SelectionPointsPanel();

        this.setSize(img.getWidth(), img.getHeight());
        this.addMouseListener(this);
        initComponents();
    }

    private void initComponents() {
        imageLabel = new JLabel(new ImageIcon(img));
        imageLabel.setSize(img.getWidth(), img.getHeight());
        selectionPanel.setSize(img.getWidth(), img.getHeight());   
        selectionPanel.setOpaque(false);
        selectionPointsPanel.setSize(img.getWidth(), img.getHeight());
        selectionPointsPanel.setOpaque(false);
        
        this.add(imageLabel, JLayeredPane.DEFAULT_LAYER);
        this.add(selectionPanel, JLayeredPane.PALETTE_LAYER);
        this.add(selectionPointsPanel, JLayeredPane.PALETTE_LAYER);
    }

    
    /*
     * Called when user is done selecting circle
     */
    private void endSelectingCircle(List<Point> points) {
        selectingCircle = false;
        setCircleCenterAndRadius(points);
        drawSelection();
        points.clear();
        
        controller.changeSelectingCircle(false);
    }
    
    /*
     * Called when user is done selecting width
     */
    private void endSelectingWidth(List<Point> points) {
        selectingWidth = false;
        setWidth(points);
        drawSelection();
        points.clear();
        
        controller.changeSelectingWidth(false);
    }
    
    /*
     * Called when user is done selecting radii
     */
    private void endSelectingRadii(List<Point> points) {
        selectingRadii = false;
        setRadii(points);
        drawSelection();
        points.clear();
        
        controller.changeSelectingRadii(false);
    }

    public void drawSelection() {
        selectionPanel.invalidate();
        selectionPanel.validate();
        selectionPanel.repaint();
    }
    
    /**
     * Sets the circle center and radius after the user has selected three
     * points along the arc of the circle
     * 
     * @param fp
     */
    private void setCircleCenterAndRadius(List<Point> points) {

        // order the points so that point[0] is the leftmost and point[2] is the
        // rightmost
        List<Point2D.Float> fPoints = orderPoints(points, new PointXCompare());

        Point2D.Float p1 = fPoints.get(0);
        Point2D.Float p2 = fPoints.get(1);
        Point2D.Float p3 = fPoints.get(2);
        float ma = (p2.y - p1.y) / (p2.x - p1.x);
        float mb = (p3.y - p2.y) / (p3.x - p2.x);
        Point2D.Float circleCenter = new Point2D.Float(0, 0);
        circleCenter.x = (ma * mb * (p1.y - p3.y) + mb * (p1.x + p2.x) - ma * (p2.x + p3.x)) / (2.0f * (mb - ma));
        circleCenter.y = -1.0f / ma * (circleCenter.x - (p1.x + p2.x) / 2.0f) + (p1.y + p2.y) / 2.0f;
        float radius = ((float) (Math.sqrt(Math.pow(p1.x - circleCenter.x, 2) + Math.pow(p1.y - circleCenter.y, 2))));
        controller.changeSelectionCircleCenter(circleCenter);
        controller.changeSelectionCircleRadius(radius);
    }
    
    /**
     * Sets the width of the ROI
     * 
     * @param points
     */
    private void setWidth(List<Point> points) {
        // order the points so that point[0] is the leftmost and point[1] is the
        // rightmost
        List<Point2D.Float> fPoints = orderPoints(points, new PointXCompare());
        controller.changeSelectionMinWidthPoint(fPoints.get(0));
        controller.changeSelectionMaxWidthPoint(fPoints.get(1));
    }
    
    /**
     * Sets the radii of the ROI
     * 
     * @param points
     */
    private void setRadii(List<Point> points) {
      // order points so that point[0] is the topmost and point[1] is the
      // bottom
      List<Point2D.Float> fPoints = orderPoints(points, new PointYCompare());
      controller.changeSelectionMinRadiusPoint(fPoints.get(0));
      controller.changeSelectionMaxRadiusPoint(fPoints.get(1));

  }

    /**
     * Utility method to order points
     * 
     * @param xPoints
     * @param yPoints
     * @param c
     * @return
     */
    private List<Point2D.Float> orderPoints(List<Point> points, Comparator<Point2D.Float> c) {
        /*
         * convert points into Point2D.Float.
         */
        List<Point2D.Float> result = new ArrayList<Point2D.Float>();
        for (int i = 0; i < points.size(); i++) {
            result.add(new Point2D.Float(points.get(i).x, points.get(i).y));
        }

        Collections.sort(result, c);

        return result;
    }

    private class PointXCompare implements Comparator<Point2D.Float> {

        public int compare(final Point2D.Float a, final Point2D.Float b) {
            if (a.x < b.x) {
                return -1;
            } else if (a.x > b.x) {
                return 1;
            } else {
                return 0;
            }
        }
    }

    private class PointYCompare implements Comparator<Point2D.Float> {

        public int compare(final Point2D.Float a, final Point2D.Float b) {
            if (a.y < b.y) {
                return -1;
            } else if (a.y > b.y) {
                return 1;
            } else {
                return 0;
            }
        }
    }
    
    @Override
    public void mousePressed(MouseEvent me) {
        //don't do anything if we arne't selecting a certain parameter
        if(!selectingCircle && !selectingWidth && !selectingRadii) {
            return;
        }
        
        curPoints.add(me.getPoint());
        
        selectionPointsPanel.invalidate();
        selectionPointsPanel.validate();
        selectionPointsPanel.repaint();

        if (selectingCircle && curPoints.size() == 3) {
            endSelectingCircle(curPoints);
        } else if (selectingWidth && curPoints.size() == 2) {
             endSelectingWidth(curPoints);
        } else if (selectingRadii && curPoints.size() == 2) {
            endSelectingRadii(curPoints);
        }
    }

    @Override
    public void mouseClicked(MouseEvent me) {
    }

    @Override
    public void mouseEntered(MouseEvent arg0) {
    }

    @Override
    public void mouseExited(MouseEvent arg0) {
    }

    @Override
    public void mouseReleased(MouseEvent arg0) {
    }

    @Override
    public void modelPropertyChange(PropertyChangeEvent evt) {
        if(evt.getPropertyName().equals(CurvilinearSelectionController.SELECTING_CIRCLE)) {
            selectingCircle = (Boolean) evt.getNewValue();
        }
        if(evt.getPropertyName().equals(CurvilinearSelectionController.SELECTING_WIDTH)) {
            selectingWidth = (Boolean) evt.getNewValue();
        }
        if(evt.getPropertyName().equals(CurvilinearSelectionController.SELECTING_RADII)) {
            selectingRadii = (Boolean) evt.getNewValue();
        }
    }
    
    /**
     * This panel is used to display the curvilinear ROI parameters.  This panel is
     * the top-most layer of the JLayeredPane
     *
     */
    private class SelectionPanel extends JPanel {

        private static final long serialVersionUID = 1L;

        SelectionPanel() {
            super();
        }

        //TODO:  this code is copied from CurvilinearHelper.getProfile() and needs to be refactored so they
        //       share code
        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.setColor(Color.YELLOW);

            if (model.getCircleCenter() != null) {
                Point2D.Float circleCenter = model.getCircleCenter();
                float radius = model.getCircleRadius();
                g.drawOval((int) (circleCenter.x - radius), (int) (circleCenter.y - radius),
                           (int) radius * 2, (int) radius * 2);

                // draw width lines
                Point2D.Float minWidthPoint = model.getMinWidthPoint();
                Point2D.Float maxWidthPoint = model.getMaxWidthPoint();
                if (minWidthPoint != null && maxWidthPoint != null) {
                    g.drawLine((int) circleCenter.x, (int) (circleCenter.y), (int) minWidthPoint.x,
                            (int) (minWidthPoint.y));
                    g.drawLine((int) circleCenter.x, (int) (circleCenter.y), (int) maxWidthPoint.x,
                            (int) (maxWidthPoint.y));
                }

                // draw radii
                Point2D.Float minRadiusPoint = model.getMinRadiusPoint();
                Point2D.Float maxRadiusPoint = model.getMaxRadiusPoint();
                if (minRadiusPoint != null && maxRadiusPoint != null) {
                    float radiusROIInner = (float) Math.sqrt(Math.pow(minRadiusPoint.x - circleCenter.x, 2)
                            + Math.pow(minRadiusPoint.y - circleCenter.y, 2));
                    float radiusROIOuter = (float) Math.sqrt(Math.pow(maxRadiusPoint.x - circleCenter.x, 2)
                            + Math.pow(maxRadiusPoint.y - circleCenter.y, 2));
                    int width = Math.round(radiusROIOuter - radiusROIInner);

                    Point2D.Float minIntersection = CurvilinearHelper.findIntersection(radiusROIInner, minWidthPoint, circleCenter, true);
                    Point2D.Float maxIntersection = CurvilinearHelper.findIntersection(radiusROIInner, maxWidthPoint, circleCenter, false);

                    int numRays = (int) Math.floor(maxIntersection.x - minIntersection.x);
                    float[] xBinsMin = new float[numRays];
                    // populate x values
                    for (int i = 0; i < xBinsMin.length; i++) {
                        xBinsMin[i] = minIntersection.x + i;
                    }

                    float[] yInnerArc = new float[numRays];
                    // solve for y values of arc based on xBinsMin
                    for (int i = 0; i < xBinsMin.length; i++) {
                        yInnerArc[i] = (circleCenter.y + (float) (Math.sqrt(Math.pow(radiusROIInner, 2)
                                - Math.pow(xBinsMin[i] - circleCenter.x, 2))));
                    }

                    float[] xBinsMax = new float[numRays];
                    float[] yOuterArc = new float[numRays];
                    for (int i = 0; i < numRays; i++) {
                        float m = (yInnerArc[i] - circleCenter.y) / (xBinsMin[i] - circleCenter.x);
                        float mag = (float) Math.sqrt(1 + Math.pow(m, 2));

                        float xN = 1.0f / mag;
                        float yN = m / mag;
                        if (m < 0) {
                            xN *= -1.0f;
                            yN *= -1.0f;
                        }

                        float xA = xBinsMin[i];
                        float yA = yInnerArc[i];

                        int counter = 0;
                        for (int r = 0; r < width; r++) {
                            float tempX = xA + r * xN;
                            float tempY = yA + r * yN;
                            counter++;
                            if (counter == (width - 1)) {
                                xBinsMax[i] = tempX;
                                yOuterArc[i] = tempY;
                                if (i == 0) {
                                    g.drawLine((int) xBinsMin[i], (int) yInnerArc[i], (int) xBinsMax[i], (int) yOuterArc[i]);
                                } else if (i > 0 && i < (numRays - 1)) {
                                    g.drawLine((int) xBinsMin[i], (int) yInnerArc[i], (int) xBinsMin[i - 1], (int) yInnerArc[i - 1]);
                                    g.drawLine((int) xBinsMax[i], (int) yOuterArc[i], (int) xBinsMax[i - 1], (int) yOuterArc[i - 1]);
                                } else if (i == (numRays - 1)) {
                                    g.drawLine((int) xBinsMin[i], (int) yInnerArc[i], (int) xBinsMax[i], (int) yOuterArc[i]);
                                }
                            }
                        }
                    }
                }
            }

        }
    }
    
    /**
     * This panel is used to render the user's selected points.
     */
    private class SelectionPointsPanel extends JPanel {
        
        private static final long serialVersionUID = 1051816704162759788L;

        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.setColor(Color.WHITE);
            
            int pointNum = 1;
            for(Point point : curPoints) {
                g.drawLine(point.x - 4, point.y, point.x + 4, point.y);
                g.drawLine(point.x, point.y - 4, point.x, point.y + 4);
                g.drawString(String.valueOf(pointNum), point.x + 5, point.y + 5);
                pointNum++;
            }
        }
        
    }
    
}
